NotImplemented and operator overloading

您所在的位置:网站首页 operator overloading python NotImplemented and operator overloading

NotImplemented and operator overloading

2023-10-01 03:25| 来源: 网络整理| 查看: 265

Note: Originally I included a lot of links to the documentation in this post, but as a new user it wouldn’t allow me to post more than two links, so I had to remove them. I hope even without those links, it’s clear what pages I’m referring to in this post.

I think the documentation about NotImplemented, object.__eq__, and operator overloading in general is just a little bit unclear.

For me, it started with this note:

Built-in Constants

[…]

NotImplemented

[…]

Note: When a binary (or in-place) method returns NotImplemented the interpreter will try the reflected operation on the other type (or some other fallback, depending on the operator). If all attempts return NotImplemented, the interpreter will raise an appropriate exception. Incorrectly returning NotImplemented will result in a misleading error message or the NotImplemented value being returned to Python code.

See Implementing the arithmetic operations for examples.

I interpreted this to mean that if “both directions” of a comparison return NotImplemented, then some kind of exception will always be raised.

However, in the case of __eq__, it seems Python will always fall back to object identity equality instead of raising any kind of exception:

>>> class A: ... def __eq__(self, other): ... print(f'A.__eq__({other!r})') ... return NotImplemented ... >>> class B: ... def __eq__(self, other): ... print(f'B.__eq__({other!r})') ... return NotImplemented ... >>> A() == B() A.__eq__() B.__eq__() False

So I guess falling back to an is check here is just one of those “other fallbacks, depending on the operator” that the note mentioned. But surely that’s documented somewhere?

Digging deeper I found this:

6.10.1. Value comparisons

[…] Because all types are (direct or indirect) subtypes of object, they inherit the default comparison behavior from object. Types can customize their comparison behavior by implementing rich comparison methods like __lt__() , described in Basic customization.

But it looks like that’s not really true:

3.3.1. Basic customization

[…]

object.__eq__(self, other)

[…] By default, object implements __eq__() by using is , returning NotImplemented in the case of a false comparison: True if x is y else NotImplemented .

If it were falling back to object.__eq__, then the falsy is check would return NotImplemented (according to the documentation on object.__eq__) and then an exception would be raised (according to the note on NotImplemented). But that’s not what happens, we just get the False result of the is check.

Furthermore, this doesn’t even seem to be describing the behavior of object itself:

>>> object() == object() False

But trying this was the epiphany for me:

>>> object.__eq__(object(), object()) NotImplemented

So the documentation for object.__eq__ is describing the behavior of object.__eq__ (duh!!) and not the behavior of a == b (not duh!!).

Indeed, digging in the documentation some more, I find:

3.3. Special method names

A class can implement certain operations that are invoked by special syntax (such as arithmetic operations or subscripting and slicing) by defining methods with special names. This is Python’s approach to operator overloading , allowing classes to define their own behavior with respect to language operators. For instance, if a class defines a method named __getitem__(), and x is an instance of this class, then x[i] is roughly equivalent to type(x).__getitem__(x, i) . Except where mentioned, attempts to execute an operation raise an exception when no appropriate method is defined (typically AttributeError or TypeError).

There’s a suggestion here that the expression containing the actual operator is only “roughly equivalent” to the corresponding magic method call. But I couldn’t find any further information on this - I would have really liked to know exactly how the two expressions differ for a particular operator. More on this later.

Back to the example: we’re overriding object.__eq__, so its behavior is entirely irrelevant. We ignore that paragraph I quoted from 6.10.1, and look at the next one:

6.10.1. Value comparisons

[…] The default behavior for equality comparison (== and != ) is based on the identity of the objects. Hence, equality comparison of instances with the same identity results in equality, and equality comparison of instances with different identities results in inequality. A motivation for this default behavior is the desire that all objects should be reflexive (i.e. x is y implies x == y ).

This seems to mean a == b actually falls back to a is b, and not to the “is check, but only if it’s True” behavior documented for object.__eq__. It looks like we’ve finally found the documentation for the behavior we’re seeing at runtime.

It turns out it’s all in the docs somewhere (mostly) - but the problem is the path I took to get there:

I found a bug in my code. IncompatibleTypeA() == IncompatibleTypeB() evaluated to False instead of raising the exception I expected. Both my __eq__ methods are returning NotImplemented for this case. Clearly I don’t understand something about how this is supposed to work. Let’s check the docs to see what’s going on. Found the note on NotImplemented, and started looking around for documentation on the fallback behavior for __eq__. Found “6.10.1 Value comparisons”, which looks like it says it inherits object.__eq__. Weird, since I’m not calling super() - but sure, let’s see what that means. Checked the documentation for object.__eq__. Sounds like I should be getting NotImplemented for a == b when a is not b. But I’m getting False, so something’s not right. Referred back to 6.10.1 and found the next paragraph that says the default is to compare the identity of the objects. This seems consistent with what I’m observing at runtime, but it seems inconsistent with what the previous paragraph just said. The documentation is just getting confusing, so it’s time to switch to “hands-on mode”. Tried out a bunch of example cases in the REPL. Finally discovered by tinkering that there is a subtle difference in meaning between a == b and a.__eq__(b). Dug around in the docs some more, and found the “roughly equivalent” wording in “3.3 Special method names”.

Understanding that subtle difference between the expression a == b and the magic method call a.__eq__(b) was critical for understanding why overriding __eq__ behaves the way it does here. But as far as I can tell, that subtle difference isn’t actually spelled out anywhere in the documentation. At best there’s a suggestion that this subtle difference might exist. I think this is the critical missing piece in the documentation here.

As a whole, the documentation strongly suggests that my example above would raise an exception, and it’s very surprising that it doesn’t. It certainly surprised me, and it seems that others found it surprising too:

python - Returning NotImplemented from __eq__ - Stack Overflow python - Why does == not raise 'TypeError not supported' when all methods return NotImplemented? - Stack Overflow

Only after much closer inspection, piecing together information found on different pages on very different topics, was it possible to form an understanding of the runtime behavior. And I’m still not exactly clear on how this affects other operators, if it affects them at all.

So I’ll suggest some rough (i.e. terrible) changes, and (if you agree that anything needs to change in the first place) we can bikeshed them into something much better.

First I suggest adding a link to the “6. Expressions” page in the NotImplemented note, since that seems to be where those “other fallbacks” are actually documented:

Built-in Constants

[…]

NotImplemented

[…]

Note: When a binary (or in-place) method returns NotImplemented the interpreter will try the reflected operation on the other type (or some other fallback, depending on the operator - see (link to "6. Expressions" page) for details on the fallback behaviors for some operators). If all attempts return NotImplemented, the interpreter will raise an appropriate exception. Incorrectly returning NotImplemented will result in a misleading error message or the NotImplemented value being returned to Python code.

See Implementing the arithmetic operations for examples.

As a documentation reader, this was my “entry point” to the docs for this whole topic, and having this link would have directed me to exactly the right place to further my understanding of the operator fallback behavior.

The more important piece is to somehow document that there is a difference between an operator-expression and the corresponding magic method call (e.g. a == b vs a.__eq__(b)). I’m not actually sure where this would best fit. In most contexts it’s going to be unnecessary detail, but in other contexts it’s going to be exactly what you need to know.

My best guess is to place a footnote in “3.3 Special method names” next to “roughly equivalent”, that leads to a separate page with details on how the expressions differ:

3.3. Special method names

A class can implement certain operations that are invoked by special syntax (such as arithmetic operations or subscripting and slicing) by defining methods with special names. This is Python’s approach to operator overloading , allowing classes to define their own behavior with respect to language operators. For instance, if a class defines a method named __getitem__(), and x is an instance of this class, then x[i] is roughly equivalent [1] to type(x).__getitem__(x, i) . Except where mentioned, attempts to execute an operation raise an exception when no appropriate method is defined (typically AttributeError or TypeError).

[…]

[1]: See (link to another page) for more details.

But this still feels fairly well-hidden. And it doesn’t really connect to the path I took through the documentation starting from NotImplemented, so it wouldn’t have actually been very helpful to the single “user story” I just described.

Does anyone else feel like this needs to be improved? Does anyone have any better suggestions for how this can be made more clear?



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3